[Jest] グローバル変数を扱うモジュールの単体テストをしたい
こんにちは、CX事業本部 Delivery部の若槻です。
前回のエントリで、再利用したいデータをAWSLambdaのグローバル変数でキャッシュするhandlerコードを紹介しました。
その後そのhandlerコードの単体テストをJestで作成しようとしたのですが、テスト対象モジュールの外にあるグローバル変数をモックさせたい場合には工夫が必要だったため、方法を書き残しておきます。
グローバル変数をモックしない場合、テストが期待通り動かない
前回のhandlerコードを少し改変した以下のコードのテストを作成してみます。処理内容は同じですが、テストのしやすさためにAWS SDKによるGetParameter処理を別関数に分けています。
import { SSMClient, GetParameterCommand, Parameter } from '@aws-sdk/client-ssm'; export let HOGE_CACHE: string | undefined; export const handler = async (): Promise<string> => { if (HOGE_CACHE !== undefined) { return HOGE_CACHE; } const parameter = await getParameter(); if (parameter !== undefined && parameter!.Value !== undefined) { const hoge = parameter.Value; HOGE_CACHE = hoge; return hoge; } HOGE_CACHE = undefined; throw new Error('Parameter value is invalid.'); }; const ssmClient = new SSMClient({ region: 'ap-northeast-1', }); export const getParameter = async (): Promise<Parameter | undefined> => { const response = await ssmClient.send( new GetParameterCommand({ Name: 'hoge' }) ); return response.Parameter; };
上記コードが期待通りの動作となるのかを確認するために、Jestで次のような3つの単体テストケースを作成してみました。
import { handler, getParameter, HOGE_CACHE } from '../lib/cdk-sample-app.nyaoFunc'; describe('キャッシュされていない場合', (): void => { describe('パラメーターがParameter Storeから取得できた場合', (): void => { test('グローバル変数にキャッシュされ、リターンされる', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: 'あああ' }); expect(HOGE_CACHE).toBeUndefined(); const response = await handler(); expect(getParameter).toBeCalledTimes(1); expect(response).toBe('あああ'); expect(HOGE_CACHE).toBe('あああ'); }); }); describe('パラメーターがParameter Storeから取得できなかった場合', (): void => { test('グローバル変数からキャッシュが削除され、エラー終了する', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: undefined }); expect(HOGE_CACHE).toBeUndefined(); await expect(() => handler()).rejects.toThrow( 'Parameter value is invalid.' ); expect(getParameter).toBeCalledTimes(1); expect(HOGE_CACHE).toBeUndefined(); }); }); }); describe('キャッシュされている場合', (): void => { test('パラメーターがグローバル変数から取得される', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: 'あああ' }); const response = await handler(); expect(getParameter).toBeCalledTimes(0); expect(response).toBe('いいい'); expect(HOGE_CACHE).toBe('いいい'); }); });
しかしテストを実行してみると、テスト毎にグローバル変数HOGE
の値がundefined
にリセットされて欲しかったのですが、1つ目のテストケースでグローバル変数に格納されたあああ
が後続のテストケースで使い回されてしまっています。
$ npx jest test/cdk-sample-app.nyaoFunc.test.ts FAIL test/cdk-sample-app.nyaoFunc.test.ts キャッシュされていない場合 パラメーターがParameter Storeから取得できた場合 ✓ グローバル変数にキャッシュされ、リターンされる (1 ms) パラメーターがParameter Storeから取得できなかった場合 ✕ グローバル変数からキャッシュが削除され、エラー終了する キャッシュされている場合 ✕ パラメーターがグローバル変数から取得される (2 ms) ● キャッシュされていない場合 › パラメーターがParameter Storeから取得できなかった場合 › グローバル変数からキャッシュが削除され、エラー終了する expect(received).toBeUndefined() Received: "あああ" 30 | .mockReturnValue({ Value: undefined }); 31 | > 32 | expect(HOGE_CACHE).toBeUndefined(); | ^ 33 | 34 | await expect(() => handler()).rejects.toThrow( 35 | 'Parameter value is invalid.' at Object.<anonymous> (test/cdk-sample-app.nyaoFunc.test.ts:32:26) ● キャッシュされている場合 › パラメーターがグローバル変数から取得される expect(received).toBe(expected) // Object.is equality Expected: "いいい" Received: "あああ" 53 | expect(getParameter).toBeCalledTimes(0); 54 | > 55 | expect(response).toBe('いいい'); | ^ 56 | 57 | expect(HOGE_CACHE).toBe('いいい'); 58 | }); at Object.<anonymous> (test/cdk-sample-app.nyaoFunc.test.ts:55:22) Test Suites: 1 failed, 1 total Tests: 2 failed, 1 passed, 3 total Snapshots: 0 total Time: 2.761 s, estimated 3 s Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.test.ts/i.
グローバル変数を操作する関数を設けてモックした場合、テストが期待通り動いた
そこでhandlerコードにグローバル変数から値を取得および設定する関数getHogeCache
とsetHogeCache
を設けて、handler内ではその関数を使用してキャッシュを操作するようにします。
import { SSMClient, GetParameterCommand, Parameter } from '@aws-sdk/client-ssm'; export let HOGE_CACHE: string | undefined; export const getHogeCache = (): string | undefined => HOGE_CACHE; export const setHogeCache = (newParameter: string | undefined): void => { HOGE_CACHE = newParameter; }; export const handler = async (): Promise<string> => { const hogeCache = getHogeCache(); if (hogeCache !== undefined) { return hogeCache; } const parameter = await getParameter(); if (parameter !== undefined && parameter!.Value !== undefined) { const hoge = parameter.Value; setHogeCache(hoge); return hoge; } setHogeCache(undefined); throw new Error('Parameter value is invalid.'); }; const ssmClient = new SSMClient({ region: 'ap-northeast-1', }); export const getParameter = async (): Promise<Parameter | undefined> => { const response = await ssmClient.send( new GetParameterCommand({ Name: 'hoge' }) ); return response.Parameter; };
テストコード側ではgetHogeCache
とsetHogeCache
をモックします。
import { handler, getParameter, getHogeCache, setHogeCache, } from '../lib/cdk-sample-app.nyaoFunc'; describe('キャッシュされていない場合', (): void => { describe('パラメーターがParameter Storeから取得できた場合', (): void => { test('グローバル変数にキャッシュされ、リターンされる', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: 'あああ' }); (getHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined); (setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined); const response = await handler(); expect(getParameter).toBeCalledTimes(1); expect(response).toBe('あああ'); expect(setHogeCache).toBeCalledTimes(1); expect(setHogeCache).toBeCalledWith('あああ'); }); }); describe('パラメーターがParameter Storeから取得できなかった場合', (): void => { test('グローバル変数からキャッシュが削除され、エラー終了する', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: undefined }); (getHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined); (setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined); await expect(() => handler()).rejects.toThrow( 'Parameter value is invalid.' ); expect(getParameter).toBeCalledTimes(1); expect(setHogeCache).toBeCalledTimes(1); expect(setHogeCache).toBeCalledWith(undefined); }); }); }); describe('キャッシュされている場合', (): void => { test('パラメーターがグローバル変数から取得される', async (): Promise<void> => { (getParameter as jest.Mock) = jest .fn() .mockReturnValue({ Value: 'あああ' }); (getHogeCache as jest.Mock) = jest.fn().mockReturnValue('いいい'); (setHogeCache as jest.Mock) = jest.fn().mockReturnValue(undefined); const response = await handler(); expect(getParameter).toBeCalledTimes(0); expect(response).toBe('いいい'); expect(setHogeCache).toBeCalledTimes(0); }); });
テストを実行すると、すべてPASSさせることができました。ちゃんとモックさせられているようです!
$ npx jest test/cdk-sample-app.nyaoFunc.test.ts PASS test/cdk-sample-app.nyaoFunc.test.ts キャッシュされていない場合 パラメーターがParameter Storeから取得できた場合 ✓ グローバル変数にキャッシュされ、リターンされる (2 ms) パラメーターがParameter Storeから取得できなかった場合 ✓ グローバル変数からキャッシュが削除され、エラー終了する (5 ms) キャッシュされている場合 ✓ パラメーターがグローバル変数から取得される Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total Snapshots: 0 total Time: 2.984 s, estimated 3 s Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.test.ts/i.
念の為、修正後のhandlerコードをAWS Lambdaにデプロイして、前回作成したE2Eテストのコードを動かしてみると、こちらもPASSしました。ウォームスタート時に上手くキャッシュが効いているようです!
$ npx jest test/cdk-sample-app.nyaoFunc.e2e.test.ts PASS test/cdk-sample-app.nyaoFunc.e2e.test.ts (10.813 s) ✓ コールドスタート (4204 ms) ✓ ウォームスタート (3130 ms) Test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 total Time: 10.875 s, estimated 11 s Ran all test suites matching /test\/cdk-sample-app.nyaoFunc.e2e.test.ts/i.
おわりに
Jestでグローバル変数を扱うモジュールの単体テストをする方法を確認しました。
結論としては、グローバル変数を操作する関数を設けてモックするようにしましょう。
以上